Skip to content

Update plugin ZTools 提供商 v1.0.1#304

Closed
Particaly wants to merge 1 commit into
ZToolsCenter:mainfrom
Particaly:plugin/f-provider
Closed

Update plugin ZTools 提供商 v1.0.1#304
Particaly wants to merge 1 commit into
ZToolsCenter:mainfrom
Particaly:plugin/f-provider

Conversation

@Particaly

Copy link
Copy Markdown
Contributor

插件信息

  • 名称: ZTools 提供商
  • 插件ID: f-provider
  • 版本: 1.0.1
  • 描述: OCR + 翻译提供商集合(百度/谷歌/有道/微软翻译、微信 OCR)
  • 作者: kaineooo
  • 类型: 更新

本次变更

  • feat: ZTools OCR + 翻译提供商插件(截图识别 / 代码翻译 / manage)
  • chore: 调整插件命名空间
  • chore: 调整资源url
  • feat: 微信 OCR 原生模块支持 macOS(libwxocr.dylib)
  • feat: 截图识别结果改为独立窗口展示(左图右文 + 拖动缩放)

截图 / 演示

自检清单

  • plugin.json 的 name / title / version / description / author 字段均已检查
  • 已移除调试日志、未使用文件、敏感信息(.env、token、密钥等)
  • 本次 PR 的 diff 仅涉及 plugins/f-provider/ 目录
  • 已在本地 ZTools 客户端实际加载并测试过此插件,主要功能正常
  • 同意以仓库声明的开源协议发布此插件

此 PR 由 ztools-plugin-cli 自动管理:每次 ztools publish 在分支上追加一个 commit,PR 链接保持不变。

- feat: ZTools OCR + 翻译提供商插件(截图识别 / 代码翻译 / manage)
- chore: 调整插件命名空间
- chore: 调整资源url
- feat: 微信 OCR 原生模块支持 macOS(libwxocr.dylib)
- feat: 截图识别结果改为独立窗口展示(左图右文 + 拖动缩放)
@Particaly Particaly closed this Jul 10, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the screenshot OCR feature by moving the result display into a dedicated, borderless sub-window (screen-ocr-result.html / ScreenOcrResult.vue), which is opened and populated dynamically by the main orchestrator (ScreenOcr.vue). Feedback focuses on improving robustness and code cleanliness: checking if the sub-window is destroyed before injecting data, properly handling asynchronous clipboard API errors, removing unused references and redundant functions (such as stageRef and decodePngSize in the result view), and binding pointer drag events to the global window object with proper cleanup to prevent stuck drag states and memory leaks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +222 to 234
function injectData(
win: BrowserWindow.WindowInstance,
data: { image: string; lines: OcrLine[]; isDark: boolean; logo?: string }
): void {
try {
// 序列化为安全的 JSON,避免引号/换行破坏 JS 字符串
const payload = JSON.stringify(data)
const code = `window.__loadScreenOcrResult && window.__loadScreenOcrResult(${payload});`
win.webContents.executeJavaScript(code)
} catch (_) {
/* ignore:兜底注入会再试 */
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在 Electron 中,如果用户在 setTimeout 的 800ms 延迟内关闭了子窗口,win 实例可能已被销毁。此时直接访问 win.webContents 或调用 executeJavaScript 会抛出异常。此外,executeJavaScript 返回一个 Promise,如果执行失败或窗口在执行期间被销毁,会产生未捕获的 Promise 拒绝(Unhandled Promise Rejection)。

建议在调用前增加 win.isDestroyed() 检查,并为 executeJavaScript 链式调用 .catch() 以优雅地捕获任何异步错误。

function injectData(
  win: BrowserWindow.WindowInstance,
  data: { image: string; lines: OcrLine[]; isDark: boolean; logo?: string }
): void {
  if (!win || win.isDestroyed()) return
  try {
    // 序列化为安全的 JSON,避免引号/换行破坏 JS 字符串
    const payload = JSON.stringify(data)
    const code = 'window.__loadScreenOcrResult && window.__loadScreenOcrResult(' + payload + ');'
    win.webContents.executeJavaScript(code).catch(() => {
      /* ignore */
    })
  } catch (_) {
    /* ignore:兜底注入会再试 */
  }
}

Comment on lines +74 to +84
function copyText(text: string) {
if (!text) return
try {
navigator.clipboard?.writeText(text)
} catch (_) {
/* 子窗口可能无 clipboard 权限,静默失败 */
}
copied.value = text
if (copiedTimer) window.clearTimeout(copiedTimer)
copiedTimer = window.setTimeout(() => (copied.value = ''), 1200)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

navigator.clipboard.writeText 是一个异步操作,返回一个 Promise。如果直接使用 try...catch 包裹它,由于它是异步执行的,任何 Promise 拒绝(例如用户未授权剪贴板权限或窗口失去焦点)都无法被同步的 try...catch 捕获,从而导致未捕获的 Promise 拒绝(Unhandled Promise Rejection)。

另外,如果 navigator.clipboard 未定义,使用 navigator.clipboard?.writeText(text).catch(...) 会因为 undefined 没有 catch 方法而抛出 TypeError。

建议先显式检查 navigator.clipboard 是否存在,然后使用 .catch() 捕获异步错误。

function copyText(text: string) {
  if (!text) return
  if (navigator.clipboard) {
    navigator.clipboard.writeText(text).catch(() => {
      /* 子窗口可能无 clipboard 权限,静默失败 */
    })
  }
  copied.value = text
  if (copiedTimer) window.clearTimeout(copiedTimer)
  copiedTimer = window.setTimeout(() => (copied.value = ''), 1200)
}

const viewportRef = ref<HTMLDivElement | null>(null)
const naturalWidth = ref(0)
const naturalHeight = ref(0)
const stageRef = ref<HTMLDivElement | null>(null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

stageRef 在脚本中定义并绑定到了模板中的元素上,但在整个脚本中从未被访问过。建议将其删除以减少无用代码。

Comment on lines +237 to +262
const el = list.children[i] as HTMLElement | undefined
if (el) {
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}
}

/**
* 点击图上文字:若是拖动结束(didDrag)则不触发,避免拖图误复制;
* 否则复制该行文字并闪烁高亮反馈。
*/
function onOverlayClick(i: number): void {
if (didDrag) {
didDrag = false
return
}
flashLine(i)
copyText(lines.value[i]?.text ?? '')
}

/** 触发第 i 行的闪烁高亮(图上 + 列表),1.5s 后移除。 */
function flashLine(i: number): void {
activeIndex.value = i
const id = (flashedIds.value[i] ?? 0) + 1
flashedIds.value[i] = id
window.setTimeout(() => {
if (flashedIds.value[i] === id) flashedIds.value[i] = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

decodePngSize 函数在 ScreenOcrResult.vue 中定义了但从未被调用(图片尺寸在 loadImage 中通过 Image 对象的 naturalWidthnaturalHeight 获取)。为了保持代码库的整洁和可维护性,建议删除此未使用的冗余函数。

Comment on lines +310 to +336
function onPointerDown(e: PointerEvent) {
if (e.button !== 0) return
pointerActive = true
didDrag = false
dragStart = { x: e.clientX, y: e.clientY }
dragOrigin = { x: offsetX.value, y: offsetY.value }
}
function onPointerMove(e: PointerEvent) {
if (!pointerActive) return
const dx = e.clientX - dragStart.x
const dy = e.clientY - dragStart.y
if (!dragging.value && Math.hypot(dx, dy) < DRAG_THRESHOLD) return
if (!dragging.value) {
dragging.value = true
didDrag = true
;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
}
offsetX.value = dragOrigin.x + dx
offsetY.value = dragOrigin.y + dy
}
function onPointerUp(e: PointerEvent) {
pointerActive = false
if (dragging.value) {
dragging.value = false
;(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

当前的拖拽逻辑将 pointermovepointeruppointercancel 事件直接绑定在 .stage 元素上。如果用户在未触发拖拽(未超过 DRAG_THRESHOLD)前快速将鼠标移出元素并释放,或者在拖拽过程中由于卡顿导致鼠标移出窗口释放,元素将无法接收到 pointerup 事件。这会导致 pointerActive 状态一直保持为 true,当鼠标再次滑入元素时,即使没有按下鼠标,也会触发非预期的拖拽行为。

建议在 pointerdown 时将 pointermovepointerup 事件动态绑定到 window 上,并在拖拽结束时移除,这样可以保证拖拽交互的绝对可靠性。

function onPointerDown(e: PointerEvent) {
  if (e.button !== 0) return
  pointerActive = true
  didDrag = false
  dragStart = { x: e.clientX, y: e.clientY }
  dragOrigin = { x: offsetX.value, y: offsetY.value }
  window.addEventListener('pointermove', onPointerMove)
  window.addEventListener('pointerup', onPointerUp)
  window.addEventListener('pointercancel', onPointerUp)
}
function onPointerMove(e: PointerEvent) {
  if (!pointerActive) return
  const dx = e.clientX - dragStart.x
  const dy = e.clientY - dragStart.y
  if (!dragging.value && Math.hypot(dx, dy) < DRAG_THRESHOLD) return
  if (!dragging.value) {
    dragging.value = true
    didDrag = true
  }
  offsetX.value = dragOrigin.x + dx
  offsetY.value = dragOrigin.y + dy
}
function onPointerUp() {
  pointerActive = false
  dragging.value = false
  window.removeEventListener('pointermove', onPointerMove)
  window.removeEventListener('pointerup', onPointerUp)
  window.removeEventListener('pointercancel', onPointerUp)
}

Comment on lines +368 to +373
onUnmounted(() => {
const vp = viewportRef.value
if (vp) vp.removeEventListener('wheel', onWheel)
window.removeEventListener('resize', onWinResize)
if (copiedTimer) window.clearTimeout(copiedTimer)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

由于拖拽事件监听器已改为动态绑定到 window,我们需要在组件卸载时(onUnmounted)一并清理这些全局监听器,以防止潜在的内存泄漏或在组件销毁后触发非预期的全局事件。

onUnmounted(() => {
  const vp = viewportRef.value
  if (vp) vp.removeEventListener('wheel', onWheel)
  window.removeEventListener('resize', onWinResize)
  window.removeEventListener('pointermove', onPointerMove)
  window.removeEventListener('pointerup', onPointerUp)
  window.removeEventListener('pointercancel', onPointerUp)
  if (copiedTimer) window.clearTimeout(copiedTimer)
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants